#include "udp_socket.h"

#include <utility> // std::move
#include <bit> // std::bit_cast

#include <sys/socket.h> // socket AF_INET SOCK_DGRAM SOCK_NONBLOCK sockaddr sendto recvfrom
#include <netinet/in.h> // sockaddr_in INADDR_ANY
#include <errno.h> // errno E*
#include <netdb.h> // addrinfo

#include "simple/support/algorithm/utils.hpp" // support::distance

namespace simple::io
{

	[[nodiscard]] socket_result open_udp_socket() noexcept
	{
		auto s = unique_handle(static_cast<handle>(socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, 0)));
		if(not s)
		{
			switch(errno)
			{
				case EACCES: return access_denied{errno};
				case EMFILE: return process_descriptor_limit_reached{errno};
				case ENFILE: return system_descriptor_limit_reached{errno};
				default: return error{errno};
			}
		}

		sockaddr_in any_address { AF_INET, INADDR_ANY, 0 };
		auto b = ::bind(static_cast<int>(static_cast<handle>(s.get())), (sockaddr*) &any_address, sizeof(any_address));
		if(b == -1)
		{
			switch(errno)
			{
				case EACCES: return access_denied{errno};
				case EADDRINUSE: return address_in_use{errno};
				case EADDRNOTAVAIL: return address_not_available{errno};
				case EINPROGRESS: assert(false && "what even is this...");
				default: return error{errno};
			}
		}

		return udp_socket{std::move(s)};
	}

	[[nodiscard]] send_result send(const udp_socket& sckt, const address& addr, byte_view message) noexcept
	{
		assert(message.valid());
		assert(message.begin() != nullptr && message.end() != nullptr);

		auto raw_addr = std::bit_cast<addrinfo*>(addr.get());
		auto result = sendto(static_cast<int>(static_cast<handle>(sckt.handle.get())),
			message.begin(), distance(message),
			0,
			raw_addr->ai_addr, raw_addr->ai_addrlen
		);
		if(-1 == result)
		{
			switch(errno)
			{
#if EWOULDBLOCK != EAGAIN
				case EWOULDBLOCK: return message.begin();
#endif
				case EAGAIN: return message.begin();
				case EAFNOSUPPORT: return address_not_supported{errno};
				case ECONNRESET: return connection_reset{errno};
				case EMSGSIZE: return message_too_long{errno};
				case EACCES: return access_denied{errno};
				case EHOSTUNREACH: return host_unreachable{errno};
				case ENETDOWN: return network_interface_down{errno};
				case ENETUNREACH: return network_unreachable{errno};

				default: return error{errno};
			}
		}

		return message.begin() + result;
	}

	[[nodiscard]] receive_result receive(const udp_socket& sckt, byte_range message) noexcept
	{
		assert(message.valid());
		assert(message.begin() != nullptr && message.end() != nullptr);

		auto result = recvfrom(static_cast<int>(static_cast<handle>(sckt.handle.get())),
			message.begin(), distance(message),
			0,
			nullptr, 0
		);
		if(-1 == result)
		{
			switch(errno)
			{
#if EWOULDBLOCK != EAGAIN
				case EWOULDBLOCK: return message.begin();
#endif
				case EAGAIN: return message.begin();
				case ECONNRESET: return connection_reset{errno};
				default: return error{errno};
			}
		}

		return message.begin() + result;
	}

} // namespace simple::io
